<?php

class System_Data_Mapper
{
    private $_tableName = null;
    private $_primaryKey = null;
    private $_adapter = null;
    private $_fields = array();
    private $_validator = null;
    private $_service = array();

    public function __construct()
    {
        $this->main();
        $this->init();
    }

    public function main()
    {
        if(!System_Registry::has('adapter')) {
            $config = System_Registry::get('config');
            $this->setAdapter($config);
            System_Registry::set('adapter', $this->getAdapter());
        } else {
            $this->_adapter = System_Registry::get('adapter');
        }
    }

    public function init()
    {
        throw new Exception('Method `init` must be defined');
    }

    public function setAdapter($config)
    {
        $dbAdapter = $config->get('db.adapter', 'mysql');
        $dbHost = $config->get('db.host', 'localhost');
        $dbName = $config->get('db.name', 'test');
        $dbUser = $config->get('db.user', 'root');
        $dbPass = $config->get('db.pass', '');
        switch($dbAdapter) {
            case 'mysql':
                $dsn = sprintf('mysql:dbname=%s;host=%s', $dbName, $dbHost);
                $this->_adapter = new System_Data_Adapter_Pdo_Mysql($dsn, $dbUser, $dbPass);
                $this->_adapter->setCurrentDbName($dbName);
            break;
            default:
                throw new Exception('Unknown adapter: ' . $dbAdapter);
            break;
        }

    }

    public function hasAdapter()
    {
        return !empty($this->_adapter);
    }

    public function getAdapter()
    {
        if(empty($this->_adapter)) {
            throw new Exception('Data adapter must be defined');
        }
        return $this->_adapter;
    }

    public function setTableName($tableName)
    {
        $this->_tableName = $tableName;
    }

    public function getTableName()
    {
        if(is_null($this->_tableName)) {
            throw new Exception('Data table name must be defined');
        }
        return $this->_tableName;
    }

    public function setPrimaryKey($primaryKey)
    {
        $this->_primaryKey = $primaryKey;
    }

    /**
     * Get name of primary key
     */
    public function getPrimaryKey()
    {
        if(is_null($this->_primaryKey)) {
            throw new Exception('Primary key must be defined');
        }
        return $this->_primaryKey;
    }

    public function setField($name, $rules)
    {
        $this->_fields[$name] = $rules;
    }

    public function setFields($fields)
    {
        $this->_fields = $fields;
    }

    public function getFields()
    {
        if(empty($this->_fields)) {
            throw new Exception('Empty fields');
        }
        return $this->_fields;
    }

    public function getValidator()
    {
        if(empty($this->_validator)) {
            $this->_validator = new System_Validate();
        }
        return $this->_validator;
    }

    public function getService($name)
    {
        if(!isset($this->_service[$name])) {
            $serviceName = str_replace('.', '_', $name);
            $this->_service[$name] = new $serviceName();
        }
        return $this->_service[$name];
    }

    public function select($fields = array())
    {
        $tableName = $this->getTableName();
        $query = new System_Data_Query($this);
        return $query->select($fields)->from($tableName);
    }

    /**
     * Find records with custom SQL query
     *
     * @param string $sql SQL query to execute
     * @param array $params Array of bound parameters to use as values for query
     * @throws Exception
     */
    public function query($sql, $params = array())
    {
        return $this->getAdapter()->query($sql, $params);
    }

    /**
     * Find first record matching given conditions
     *
     * @param array $conditions Array of conditions in column => value pairs
     */
    public function get($conditions, $fields = array())
    {
        return $this->getAdapter()->get($this->getTableName(), $conditions, $fields);
    }

    /**
     * Create record
     *
     * @param array $fields array of field => value pairs
     */
    public function insert($fields)
    {
        return $this->getAdapter()->insert($this->getTableName(), $fields);
        if($this->validate($fields)) {
            return $this->getAdapter()->insert($this->getTableName(), $fields);
        } else {
            return false;
        }
    }

    /**
     * Update items matching given conditions
     *
     * @param array $conditions Array of conditions in column => value pairs
     * @param array $entity Array of field => value pairs
     */
    public function update($entity = array(), $conditions)
    {
        return $this->getAdapter()->create($this->getTableName(), $entity);
        if($this->validate($entity)) {
            return $this->getAdapter()->create($this->getTableName(), $entity);
        } else {
            return false;
        }
    }

    /**
     * Delete items matching given conditions
     *
     * @param array $conditions Array of conditions in column => value pairs
     */
    public function delete($conditions = array())
    {
        return $this->getAdapter()->delete($this->getTableName(), $conditions);
    }

    /**
     * Check exists matching given conditions
     *
     * @param array $conditions Array of conditions in column => value pairs
     */
    public function exists($conditions = array())
    {
        return $this->getAdapter()->exists($this->getTableName(), $conditions);
    }

    /**
     * Count items matching given conditions
     *
     * @param array $conditions Array of conditions in column => value pairs
     */
    public function count($conditions = null)
    {
        return $this->getAdapter()->count($this->getTableName(), $conditions);
    }

    /**
     * Load record from primary key
     */
    public function getByPrimary($primaryKeyValue, $fields = array())
    {
        $primaryKeyName = $this->getPrimaryKey();
        return $this->get(array($primaryKeyName => $primaryKeyValue), $fields);
    }

    /**
     * Update items by primary key
     *
     * @param array $conditions primary key value
     * @param array $entity Array of field => value pairs
     */
    public function updateByPrimaryKey($entity = array(), $primaryKey)
    {
        $primaryKeyName = $this->getPrimaryKey();
        return $this->update($entity, array($primaryKeyName => $primaryKeyValue));
    }

    /**
     * Delete items by primary key
     *
     * @param array $conditions Array of conditions in column => value pairs
     */
    public function deleteByPrimaryKey($primaryKey)
    {
        $primaryKeyName = $this->getPrimaryKey();
        return $this->delete(array($primaryKeyName => $primaryKeyValue));
    }

    /**
     * Find records with given conditions
     * If all parameters are empty, find all records
     *
     * @param array $conditions Array of conditions in column => value pairs
     */
    public function find($conditions = null, $fields = null, $order = null, $limit = 1000)
    {
        if(empty($order)) {
            $order = array($this->getPrimaryKey() => 'desc');
        }

        return $this->getAdapter()->find($this->getTableName(), $conditions, $fields, $order, 0, $limit);
    }

    /**
     * Find records with given conditions
     * If all parameters are empty, find all records
     *
     * @param array $conditions Array of conditions in column => value pairs
     */
    public function findByPage($conditions = null, $fields = null, $order = null, $currentPage = 1, $pageSize = 20)
    {
        if(empty($order)) {
            $order = array($this->getPrimaryKey() => 'desc');
        }

        // Count records
        $totalRecords = $this->count($conditions);
        if(empty($totalRecords)) {
            return array('record' => null, 'pager' => null);
        }

        // Cal page
        $pageSize = empty($pageSize) ? 20 : $pageSize;
        if($pageSize > 1000) {
            throw new InvalidArgumentException('Page size limit is 1000');
        }
        $pager = new System_Data_Pager($totalRecords, $currentPage, $pageSize);
        $pageIndex = $pager->getPageIndex();

        // Get result
        $result = array();
        $adapter = $this->getAdapter();
        $result['record'] = $adapter->find($this->getTableName(), $conditions, $fields, $order, $pageIndex, $pageSize);
        $result['pager'] = $pager->getParams();
        return $result;
    }

    /**
     * Run set validation rules on fields
     */
    public function _validate($entity)
    {
        // Check validation rules on each feild
        $validator = new System_Validate();
        foreach($entity as $field => $value) {
            if(!isset($this->_fields[$field])) {
                throw new Exception('Undefined field name: ' . $field);
            }
            $method = '';
            $message = '';
            $param = '';
            $validator->validate($field, $value, $method, $param, $message);
        }
        return $validator->isValid();
    }

    public function validate($data, $field, $method, $value, $message)
    {
        $validator = $this->getValidator();
        $validator->valid($data, $method, $value);
        if(!$validator->valid($data, $method, $value)) {
            // throw new InvalidArgumentException('Invalid argument '.$field.': ' . $message);
            throw new InvalidArgumentException($message);
        }
        return true;
    }

    /**
     * Create table structure changes from model to database
     */
    public function createTable()
    {
        return $this->getAdapter()->createTable($this->getTableName(), $this->getFields());
    }

    /**
     * Drop/delete data table
     * Destructive and dangerous - drops entire data source and all data
     */
    public function dropTable()
    {
        return $this->getAdapter()->dropTable($this->getTableName());
    }

    /**
     * Truncate data table
     * Should delete all rows and reset serial/auto_increment keys to 0
     */
    public function truncateTable()
    {
        return $this->getAdapter()->truncateTable($this->getTableName());
    }

    public function addColumn()
    {

    }

    public function removeColumn()
    {

    }

}
